luci-mod-network: organize "dnsrecords" tab in dhcp/dns views
authorDavid Härdeman <[email protected]>
Wed, 15 Oct 2025 16:21:58 +0000 (18:21 +0200)
committerPaul Donald <[email protected]>
Sat, 18 Oct 2025 21:51:38 +0000 (23:51 +0200)
Delete the tab from dhcp.js, sort the options that belong to it in dns.js.

Signed-off-by: David Härdeman <[email protected]>
modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js
modules/luci-mod-network/htdocs/luci-static/resources/view/network/dns.js

index f029a3a7b8b55c88b26f599c4118fd4930cfca81..efd94d25865e60dab9499ab443cf338a882e3bc7 100644 (file)
@@ -8,7 +8,6 @@
 'require network';
 'require validation';
 'require tools.widgets as widgets';
-'require tools.dnsrecordhandlers as drh';
 
 var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status;
 var callUfpList;
@@ -294,7 +293,7 @@ return view.extend({
 
        render: function([hosts, duids, pools, networks, macdata]) {
                var has_dhcpv6 = L.hasSystemFeature('dnsmasq', 'dhcpv6') || L.hasSystemFeature('odhcpd'),
-                   m, s, o, ss, so, dnss;
+                   m, s, o, ss, so;
 
                let noi18nstrings = {
                        etc_hosts: '<code>/etc/hosts</code>',
@@ -392,7 +391,6 @@ return view.extend({
 
                s.tab('general', _('General'));
                s.tab('devices', _('Devices &amp; Ports'));
-               s.tab('dnsrecords', _('DNS Records'));
                s.tab('dnssecopt', _('DNSSEC'));
                s.tab('filteropts', _('Filter'));
                s.tab('forward', _('Forwards'));
@@ -903,123 +901,6 @@ return view.extend({
                so.value('39', _('40: LoongArch 64-bit UEFI boot from HTTP'));
                so.value('41', _('41: ARM rpiboot'));
 
-               o = s.taboption('dnsrecords', form.SectionValue, '__dnsrecords__', form.TypedSection, '__dnsrecords__');
-
-               dnss = o.subsection;
-
-               dnss.anonymous = true;
-               dnss.cfgsections = function() { return [ '__dnsrecords__' ] };
-
-               dnss.tab('hosts', _('Hostnames'));
-               dnss.tab('srvhosts', _('SRV'));
-               dnss.tab('mxhosts', _('MX'));
-               dnss.tab('cnamehosts', _('CNAME'));
-               dnss.tab('dnsrr', _('DNS-RR'));
-
-               o = dnss.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
-                       _('Bind service records to a domain name: specify the location of services. See <a href="%s">RFC2782</a>.').format('https://datatracker.ietf.org/doc/html/rfc2782')
-                       + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
-                       + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
-                       + '<br />' + _('You may add multiple records for the same Target.')
-                       + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-               ss.rowcolors = true;
-
-               so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax:') + ' ' + '<code>_service._proto.example.com.</code>');
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = '_sip._tcp.example.com.';
-
-               so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'sip.example.com.';
-
-               so = ss.option(form.Value, 'port', _('Port'));
-               so.rmempty = false;
-               so.datatype = 'port';
-               so.placeholder = '5060';
-
-               so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
-               so.rmempty = true;
-               so.datatype = 'range(0,65535)';
-               so.placeholder = '10';
-
-               so = ss.option(form.Value, 'weight', _('Weight'));
-               so.rmempty = true;
-               so.datatype = 'range(0,65535)';
-               so.placeholder = '50';
-
-               o = dnss.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
-                       _('Bind service records to a domain name: specify the location of services.')
-                        + '<br />' + _('You may add multiple records for the same domain.'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-               ss.rowcolors = true;
-               ss.nodescriptions = true;
-
-               so = ss.option(form.Value, 'domain', _('Domain'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'example.com.';
-
-               so = ss.option(form.Value, 'relay', _('Relay'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'relay.example.com.';
-
-               so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
-               so.rmempty = true;
-               so.datatype = 'range(0,65535)';
-               so.placeholder = '0';
-
-               o = dnss.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
-                       _('Set an alias for a hostname.'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-               ss.rowcolors = true;
-               ss.nodescriptions = true;
-
-               so = ss.option(form.Value, 'cname', _('Domain'));
-               so.rmempty = false;
-               so.validate = validateHostname;
-               so.placeholder = 'www.example.com.';
-
-               so = ss.option(form.Value, 'target', _('Target'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'example.com.';
-
-               o = dnss.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
-                       _('Hostnames are used to bind a domain name to an IP address. This setting is redundant for hostnames already configured with static leases, but it can be useful to rebind an FQDN.'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-
-               so = ss.option(form.Value, 'name', _('Hostname'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-
-               so = ss.option(form.Value, 'ip', _('IP address'));
-               so.rmempty = false;
-               so.datatype = 'ipaddr("nomask")';
-
                var ipaddrs = {};
 
                Object.keys(hosts).forEach(function(mac) {
@@ -1029,126 +910,6 @@ return view.extend({
                                ipaddrs[addrs[i]] = hosts[mac].name || mac;
                });
 
-               L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
-                       so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
-               });
-
-               o = dnss.taboption('dnsrr', form.SectionValue, '__dnsrr__', form.GridSection, 'dnsrr', null, 
-                       _('Set an arbitrary resource record (RR) type.') + '<br/>' + 
-                       _('Hexdata is automatically en/decoded on save and load'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-               ss.rowcolors = true;
-               ss.nodescriptions = true;
-
-               function hexdecodeload(section_id) {
-                       let value = uci.get('dhcp', section_id, 'hexdata') || '';
-                       // Remove any spaces or colons from the hex string - they're allowed
-                       value = value.replace(/[\s:]/g, '');
-                       // Hex-decode the string before displaying
-                       let decodedString = '';
-                       for (let i = 0; i < value.length; i += 2) {
-                               decodedString += String.fromCharCode(parseInt(value.substr(i, 2), 16));
-                       }
-                       return decodedString;
-               }
-
-               function hexencodesave(section, value) {
-                       if (!value || value.length === 0) {
-                               uci.unset('dhcp', section, 'hexdata');
-                               return;
-                       }
-                       // Hex-encode the string before saving
-                       const encodedArr = value.split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');
-                       uci.set('dhcp', section, this.option, encodedArr);
-               }
-
-               so = ss.option(form.Value, 'rrname', _('Resource Record Name'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'svcb.example.com.';
-
-               so = ss.option(form.Value, 'rrnumber', _('Resource Record Number'));
-               so.rmempty = false;
-               so.datatype = 'uinteger';
-               so.placeholder = '64';
-
-               so = ss.option(form.Value, '_hexdata', _('Raw Data'));
-               so.rmempty = true;
-               so.datatype = 'string';
-               so.placeholder = 'free-form string';
-               so.load = hexdecodeload;
-               so.write = hexencodesave;
-               so.modalonly = true;
-               so.depends({ rrnumber: '65', '!reverse': true });
-
-               so = ss.option(form.DummyValue, 'hexdata', _('Hex Data'));
-               so.width = '50%';
-               so.rawhtml = true;
-               so.load = function(section_id) {
-                       let hexdata = uci.get('dhcp', section_id, 'hexdata') || '';
-                       hexdata = hexdata.replace(/[:]/g, '');
-                       return hexdata.replace(/(.{2})/g, '$1 ');
-               };
-
-               function writetype65(section_id, value) {
-                       let rrnum = uci.get('dhcp', section_id, 'rrnumber');
-                       if (rrnum !== '65') return;
-
-                       let priority = parseInt(this.section.formvalue(section_id, '_svc_priority'), 10);
-                       let target = this.section.formvalue(section_id, '_svc_target') || '.';
-                       let params = value.trim().split('\n').map(l => l.trim()).filter(Boolean);
-
-                       const hex = drh.buildSvcbHex(priority, target, params); 
-                       uci.set('dhcp', section_id, 'hexdata', hex);
-               };
-
-               function loadtype65(section_id) {
-                       let rrnum = uci.get('dhcp', section_id, 'rrnumber');
-                       if (rrnum !== '65') return null;
-
-                       let hexdata = uci.get('dhcp', section_id, 'hexdata');
-                       return drh.parseSvcbHex(hexdata);
-               };
-
-               // Type 65 builder fields (hidden unless rrnumber === 65)
-               so = ss.option(form.Value, '_svc_priority', _('Svc Priority'));
-               so.placeholder = 1;
-               so.datatype = 'and(uinteger,min(0),max(65535))'
-               so.modalonly = true;
-               so.depends({ rrnumber: '65' });
-               so.write = writetype65;
-               so.load = function(section_id) {
-                       const parsed = loadtype65(section_id);
-                       return parsed?.priority?.toString() || '';
-               };
-
-               so = ss.option(form.Value, '_svc_target', _('Svc Target'));
-               so.placeholder = 'svc.example.com.';
-               so.dataype = 'hostname';
-               so.modalonly = true;
-               so.depends({ rrnumber: '65' });
-               so.write = writetype65;
-               so.load = function(section_id) {
-                       const parsed = loadtype65(section_id);
-                       return parsed?.target || '';
-               };
-
-               so = ss.option(form.TextValue, '_svc_params', _('Svc Parameters'));
-               so.placeholder = 'alpn=h2,h3\nipv4hint=192.0.2.1,192.0.2.2\nipv6hint=2001:db8::1,2001:db8::2\nport=8000';
-               so.modalonly = true;
-               so.rows = 4;
-               so.depends({ rrnumber: '65' });
-               so.write = writetype65;
-               so.load = function(section_id) {
-                       const parsed = loadtype65(section_id);
-                       return parsed?.params?.join('\n') || '';
-               };
-
                o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
                        _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.') + '<br />' +
                        _('The netfilter components below are only regarded when running fw4.'));
index 9818181e86d726c763dcf980a9b65381435a51ce..50dbd220abcd7b576c194f2d41374a0dadbc14fb 100644 (file)
@@ -521,6 +521,254 @@ return view.extend({
                o.depends('queryport', '');
                // End devices
 
+               // Begin dnsrecords
+               o = s.taboption('dnsrecords', form.SectionValue, '__dnsrecords__', form.TypedSection, '__dnsrecords__');
+
+               dnss = o.subsection;
+
+               dnss.anonymous = true;
+               dnss.cfgsections = function() { return [ '__dnsrecords__' ] };
+
+               dnss.tab('hosts', _('Hostnames'));
+               dnss.tab('srvhosts', _('SRV'));
+               dnss.tab('mxhosts', _('MX'));
+               dnss.tab('cnamehosts', _('CNAME'));
+               dnss.tab('dnsrr', _('DNS-RR'));
+
+               o = dnss.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
+                       _('Bind service records to a domain name: specify the location of services. See <a href="%s">RFC2782</a>.').format('https://datatracker.ietf.org/doc/html/rfc2782')
+                       + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
+                       + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
+                       + '<br />' + _('You may add multiple records for the same Target.')
+                       + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+               ss.rowcolors = true;
+
+               so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax:') + ' ' + '<code>_service._proto.example.com.</code>');
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = '_sip._tcp.example.com.';
+
+               so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'sip.example.com.';
+
+               so = ss.option(form.Value, 'port', _('Port'));
+               so.rmempty = false;
+               so.datatype = 'port';
+               so.placeholder = '5060';
+
+               so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
+               so.rmempty = true;
+               so.datatype = 'range(0,65535)';
+               so.placeholder = '10';
+
+               so = ss.option(form.Value, 'weight', _('Weight'));
+               so.rmempty = true;
+               so.datatype = 'range(0,65535)';
+               so.placeholder = '50';
+
+               o = dnss.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
+                       _('Bind service records to a domain name: specify the location of services.')
+                        + '<br />' + _('You may add multiple records for the same domain.'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+               ss.rowcolors = true;
+               ss.nodescriptions = true;
+
+               so = ss.option(form.Value, 'domain', _('Domain'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'example.com.';
+
+               so = ss.option(form.Value, 'relay', _('Relay'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'relay.example.com.';
+
+               so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
+               so.rmempty = true;
+               so.datatype = 'range(0,65535)';
+               so.placeholder = '0';
+
+               o = dnss.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
+                       _('Set an alias for a hostname.'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+               ss.rowcolors = true;
+               ss.nodescriptions = true;
+
+               so = ss.option(form.Value, 'cname', _('Domain'));
+               so.rmempty = false;
+               so.validate = validateHostname;
+               so.placeholder = 'www.example.com.';
+
+               so = ss.option(form.Value, 'target', _('Target'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'example.com.';
+
+               o = dnss.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
+                       _('Hostnames are used to bind a domain name to an IP address. This setting is redundant for hostnames already configured with static leases, but it can be useful to rebind an FQDN.'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+
+               so = ss.option(form.Value, 'name', _('Hostname'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+
+               so = ss.option(form.Value, 'ip', _('IP address'));
+               so.rmempty = false;
+               so.datatype = 'ipaddr("nomask")';
+
+               var ipaddrs = {};
+
+               Object.keys(hosts).forEach(function(mac) {
+                       var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
+
+                       for (var i = 0; i < addrs.length; i++)
+                               ipaddrs[addrs[i]] = hosts[mac].name || mac;
+               });
+
+               L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
+                       so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
+               });
+
+               o = dnss.taboption('dnsrr', form.SectionValue, '__dnsrr__', form.GridSection, 'dnsrr', null, 
+                       _('Set an arbitrary resource record (RR) type.') + '<br/>' + 
+                       _('Hexdata is automatically en/decoded on save and load'));
+
+               ss = o.subsection;
+
+               ss.addremove = true;
+               ss.anonymous = true;
+               ss.sortable  = true;
+               ss.rowcolors = true;
+               ss.nodescriptions = true;
+
+               function hexdecodeload(section_id) {
+                       let value = uci.get('dhcp', section_id, 'hexdata') || '';
+                       // Remove any spaces or colons from the hex string - they're allowed
+                       value = value.replace(/[\s:]/g, '');
+                       // Hex-decode the string before displaying
+                       let decodedString = '';
+                       for (let i = 0; i < value.length; i += 2) {
+                               decodedString += String.fromCharCode(parseInt(value.substr(i, 2), 16));
+                       }
+                       return decodedString;
+               }
+
+               function hexencodesave(section, value) {
+                       if (!value || value.length === 0) {
+                               uci.unset('dhcp', section, 'hexdata');
+                               return;
+                       }
+                       // Hex-encode the string before saving
+                       const encodedArr = value.split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');
+                       uci.set('dhcp', section, this.option, encodedArr);
+               }
+
+               so = ss.option(form.Value, 'rrname', _('Resource Record Name'));
+               so.rmempty = false;
+               so.datatype = 'hostname';
+               so.placeholder = 'svcb.example.com.';
+
+               so = ss.option(form.Value, 'rrnumber', _('Resource Record Number'));
+               so.rmempty = false;
+               so.datatype = 'uinteger';
+               so.placeholder = '64';
+
+               so = ss.option(form.Value, '_hexdata', _('Raw Data'));
+               so.rmempty = true;
+               so.datatype = 'string';
+               so.placeholder = 'free-form string';
+               so.load = hexdecodeload;
+               so.write = hexencodesave;
+               so.modalonly = true;
+               so.depends({ rrnumber: '65', '!reverse': true });
+
+               so = ss.option(form.DummyValue, 'hexdata', _('Hex Data'));
+               so.width = '50%';
+               so.rawhtml = true;
+               so.load = function(section_id) {
+                       let hexdata = uci.get('dhcp', section_id, 'hexdata') || '';
+                       hexdata = hexdata.replace(/[:]/g, '');
+                       return hexdata.replace(/(.{2})/g, '$1 ');
+               };
+
+               function writetype65(section_id, value) {
+                       let rrnum = uci.get('dhcp', section_id, 'rrnumber');
+                       if (rrnum !== '65') return;
+
+                       let priority = parseInt(this.section.formvalue(section_id, '_svc_priority'), 10);
+                       let target = this.section.formvalue(section_id, '_svc_target') || '.';
+                       let params = value.trim().split('\n').map(l => l.trim()).filter(Boolean);
+
+                       const hex = drh.buildSvcbHex(priority, target, params); 
+                       uci.set('dhcp', section_id, 'hexdata', hex);
+               };
+
+               function loadtype65(section_id) {
+                       let rrnum = uci.get('dhcp', section_id, 'rrnumber');
+                       if (rrnum !== '65') return null;
+
+                       let hexdata = uci.get('dhcp', section_id, 'hexdata');
+                       return drh.parseSvcbHex(hexdata);
+               };
+
+               // Type 65 builder fields (hidden unless rrnumber === 65)
+               so = ss.option(form.Value, '_svc_priority', _('Svc Priority'));
+               so.placeholder = 1;
+               so.datatype = 'and(uinteger,min(0),max(65535))'
+               so.modalonly = true;
+               so.depends({ rrnumber: '65' });
+               so.write = writetype65;
+               so.load = function(section_id) {
+                       const parsed = loadtype65(section_id);
+                       return parsed?.priority?.toString() || '';
+               };
+
+               so = ss.option(form.Value, '_svc_target', _('Svc Target'));
+               so.placeholder = 'svc.example.com.';
+               so.dataype = 'hostname';
+               so.modalonly = true;
+               so.depends({ rrnumber: '65' });
+               so.write = writetype65;
+               so.load = function(section_id) {
+                       const parsed = loadtype65(section_id);
+                       return parsed?.target || '';
+               };
+
+               so = ss.option(form.TextValue, '_svc_params', _('Svc Parameters'));
+               so.placeholder = 'alpn=h2,h3\nipv4hint=192.0.2.1,192.0.2.2\nipv6hint=2001:db8::1,2001:db8::2\nport=8000';
+               so.modalonly = true;
+               so.rows = 4;
+               so.depends({ rrnumber: '65' });
+               so.write = writetype65;
+               so.load = function(section_id) {
+                       const parsed = loadtype65(section_id);
+                       return parsed?.params?.join('\n') || '';
+               };
+               // End dnsrecords
+
                s.taboption('filteropts', form.Flag, 'domainneeded',
                        _('Domain required'),
                        _('Never forward DNS queries which lack dots or domain parts.') + '<br />' +
@@ -982,252 +1230,6 @@ return view.extend({
                so.value('39', _('40: LoongArch 64-bit UEFI boot from HTTP'));
                so.value('41', _('41: ARM rpiboot'));
 
-               o = s.taboption('dnsrecords', form.SectionValue, '__dnsrecords__', form.TypedSection, '__dnsrecords__');
-
-               dnss = o.subsection;
-
-               dnss.anonymous = true;
-               dnss.cfgsections = function() { return [ '__dnsrecords__' ] };
-
-               dnss.tab('hosts', _('Hostnames'));
-               dnss.tab('srvhosts', _('SRV'));
-               dnss.tab('mxhosts', _('MX'));
-               dnss.tab('cnamehosts', _('CNAME'));
-               dnss.tab('dnsrr', _('DNS-RR'));
-
-               o = dnss.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
-                       _('Bind service records to a domain name: specify the location of services. See <a href="%s">RFC2782</a>.').format('https://datatracker.ietf.org/doc/html/rfc2782')
-                       + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
-                       + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
-                       + '<br />' + _('You may add multiple records for the same Target.')
-                       + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-               ss.rowcolors = true;
-
-               so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax:') + ' ' + '<code>_service._proto.example.com.</code>');
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = '_sip._tcp.example.com.';
-
-               so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'sip.example.com.';
-
-               so = ss.option(form.Value, 'port', _('Port'));
-               so.rmempty = false;
-               so.datatype = 'port';
-               so.placeholder = '5060';
-
-               so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
-               so.rmempty = true;
-               so.datatype = 'range(0,65535)';
-               so.placeholder = '10';
-
-               so = ss.option(form.Value, 'weight', _('Weight'));
-               so.rmempty = true;
-               so.datatype = 'range(0,65535)';
-               so.placeholder = '50';
-
-               o = dnss.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
-                       _('Bind service records to a domain name: specify the location of services.')
-                        + '<br />' + _('You may add multiple records for the same domain.'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-               ss.rowcolors = true;
-               ss.nodescriptions = true;
-
-               so = ss.option(form.Value, 'domain', _('Domain'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'example.com.';
-
-               so = ss.option(form.Value, 'relay', _('Relay'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'relay.example.com.';
-
-               so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
-               so.rmempty = true;
-               so.datatype = 'range(0,65535)';
-               so.placeholder = '0';
-
-               o = dnss.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
-                       _('Set an alias for a hostname.'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-               ss.rowcolors = true;
-               ss.nodescriptions = true;
-
-               so = ss.option(form.Value, 'cname', _('Domain'));
-               so.rmempty = false;
-               so.validate = validateHostname;
-               so.placeholder = 'www.example.com.';
-
-               so = ss.option(form.Value, 'target', _('Target'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'example.com.';
-
-               o = dnss.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
-                       _('Hostnames are used to bind a domain name to an IP address. This setting is redundant for hostnames already configured with static leases, but it can be useful to rebind an FQDN.'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-
-               so = ss.option(form.Value, 'name', _('Hostname'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-
-               so = ss.option(form.Value, 'ip', _('IP address'));
-               so.rmempty = false;
-               so.datatype = 'ipaddr("nomask")';
-
-               var ipaddrs = {};
-
-               Object.keys(hosts).forEach(function(mac) {
-                       var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
-
-                       for (var i = 0; i < addrs.length; i++)
-                               ipaddrs[addrs[i]] = hosts[mac].name || mac;
-               });
-
-               L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
-                       so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
-               });
-
-               o = dnss.taboption('dnsrr', form.SectionValue, '__dnsrr__', form.GridSection, 'dnsrr', null, 
-                       _('Set an arbitrary resource record (RR) type.') + '<br/>' + 
-                       _('Hexdata is automatically en/decoded on save and load'));
-
-               ss = o.subsection;
-
-               ss.addremove = true;
-               ss.anonymous = true;
-               ss.sortable  = true;
-               ss.rowcolors = true;
-               ss.nodescriptions = true;
-
-               function hexdecodeload(section_id) {
-                       let value = uci.get('dhcp', section_id, 'hexdata') || '';
-                       // Remove any spaces or colons from the hex string - they're allowed
-                       value = value.replace(/[\s:]/g, '');
-                       // Hex-decode the string before displaying
-                       let decodedString = '';
-                       for (let i = 0; i < value.length; i += 2) {
-                               decodedString += String.fromCharCode(parseInt(value.substr(i, 2), 16));
-                       }
-                       return decodedString;
-               }
-
-               function hexencodesave(section, value) {
-                       if (!value || value.length === 0) {
-                               uci.unset('dhcp', section, 'hexdata');
-                               return;
-                       }
-                       // Hex-encode the string before saving
-                       const encodedArr = value.split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');
-                       uci.set('dhcp', section, this.option, encodedArr);
-               }
-
-               so = ss.option(form.Value, 'rrname', _('Resource Record Name'));
-               so.rmempty = false;
-               so.datatype = 'hostname';
-               so.placeholder = 'svcb.example.com.';
-
-               so = ss.option(form.Value, 'rrnumber', _('Resource Record Number'));
-               so.rmempty = false;
-               so.datatype = 'uinteger';
-               so.placeholder = '64';
-
-               so = ss.option(form.Value, '_hexdata', _('Raw Data'));
-               so.rmempty = true;
-               so.datatype = 'string';
-               so.placeholder = 'free-form string';
-               so.load = hexdecodeload;
-               so.write = hexencodesave;
-               so.modalonly = true;
-               so.depends({ rrnumber: '65', '!reverse': true });
-
-               so = ss.option(form.DummyValue, 'hexdata', _('Hex Data'));
-               so.width = '50%';
-               so.rawhtml = true;
-               so.load = function(section_id) {
-                       let hexdata = uci.get('dhcp', section_id, 'hexdata') || '';
-                       hexdata = hexdata.replace(/[:]/g, '');
-                       return hexdata.replace(/(.{2})/g, '$1 ');
-               };
-
-               function writetype65(section_id, value) {
-                       let rrnum = uci.get('dhcp', section_id, 'rrnumber');
-                       if (rrnum !== '65') return;
-
-                       let priority = parseInt(this.section.formvalue(section_id, '_svc_priority'), 10);
-                       let target = this.section.formvalue(section_id, '_svc_target') || '.';
-                       let params = value.trim().split('\n').map(l => l.trim()).filter(Boolean);
-
-                       const hex = drh.buildSvcbHex(priority, target, params); 
-                       uci.set('dhcp', section_id, 'hexdata', hex);
-               };
-
-               function loadtype65(section_id) {
-                       let rrnum = uci.get('dhcp', section_id, 'rrnumber');
-                       if (rrnum !== '65') return null;
-
-                       let hexdata = uci.get('dhcp', section_id, 'hexdata');
-                       return drh.parseSvcbHex(hexdata);
-               };
-
-               // Type 65 builder fields (hidden unless rrnumber === 65)
-               so = ss.option(form.Value, '_svc_priority', _('Svc Priority'));
-               so.placeholder = 1;
-               so.datatype = 'and(uinteger,min(0),max(65535))'
-               so.modalonly = true;
-               so.depends({ rrnumber: '65' });
-               so.write = writetype65;
-               so.load = function(section_id) {
-                       const parsed = loadtype65(section_id);
-                       return parsed?.priority?.toString() || '';
-               };
-
-               so = ss.option(form.Value, '_svc_target', _('Svc Target'));
-               so.placeholder = 'svc.example.com.';
-               so.dataype = 'hostname';
-               so.modalonly = true;
-               so.depends({ rrnumber: '65' });
-               so.write = writetype65;
-               so.load = function(section_id) {
-                       const parsed = loadtype65(section_id);
-                       return parsed?.target || '';
-               };
-
-               so = ss.option(form.TextValue, '_svc_params', _('Svc Parameters'));
-               so.placeholder = 'alpn=h2,h3\nipv4hint=192.0.2.1,192.0.2.2\nipv6hint=2001:db8::1,2001:db8::2\nport=8000';
-               so.modalonly = true;
-               so.rows = 4;
-               so.depends({ rrnumber: '65' });
-               so.write = writetype65;
-               so.load = function(section_id) {
-                       const parsed = loadtype65(section_id);
-                       return parsed?.params?.join('\n') || '';
-               };
-
                o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
                        _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.') + '<br />' +
                        _('The netfilter components below are only regarded when running fw4.'));